package it.polito.connectedcar.message;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;

public class Storage {
	private List<Byte> storageList;
	private int position;
	private ListIterator<Byte> listIt;

	public Storage() {
		storageList = new ArrayList<Byte>();
		init();
	}

	public Storage(byte[] packet) {
		this(packet, 0, packet.length);
	}

	public Storage(byte[] packet, int offset, int length) {
		if (packet == null)
			throw new NullPointerException("packet can't be null");
		if (length > packet.length)
			throw new IndexOutOfBoundsException("length exceeds packet length");
		if (offset + length > packet.length)
			throw new IndexOutOfBoundsException("content is outside the array");

		storageList = new ArrayList<Byte>();

		for (int i = offset; i < length; i++) {
			writeByte(packet[i]);
		}

		init();
	}

	public Storage(short[] packet) {
		this(packet, 0, packet.length);
	}

	public Storage(short[] packet, int offset, int length) {
		if (packet == null)
			throw new NullPointerException("packet can't be null");
		if (length > packet.length)
			throw new IndexOutOfBoundsException("length exceeds packet length");
		if (offset + length > packet.length)
			throw new IndexOutOfBoundsException("content is outside the array");

		storageList = new ArrayList<Byte>();

		for (int i = offset; i < length; i++) {
			writeByte(packet[i]);
		}

		init();
	}

	public boolean validPos() {
		return (position < storageList.size() && position >= 0 && storageList
				.size() != 0);
	}

	public int position() {
		return position;
	}

	/**
	 * Write a byte value to the List a signed value will be converted to its
	 * unsigned equivalent first
	 * 
	 * @param value
	 *            the byte to be written
	 */
	public void writeByte(short value) throws IllegalArgumentException {
		writeByte((int) value);
	}

	public void writeByte(int value) throws IllegalArgumentException {
		if (value < -128 || value > 127)
			throw new IllegalArgumentException(
					"Error writing byte: byte value may only range from -128 to 127.");

		storageList.add(new Byte((byte) (value)));
	}

	/**
	 * Read a byte value from the List
	 * 
	 * @return the read byte as an Integer value (unsigned)
	 */
	public short readByte() throws IllegalStateException {
		if (!validPos())
			throw new IllegalStateException(
					"Error reading byte, invalid list position specified for reading: "
							+ position);

		position++;
		return (short) listIt.next();

	}

	public void writeUnsignedByte(short value) throws IllegalArgumentException {
		writeUnsignedByte((int) value);
	}

	public void writeUnsignedByte(int value) throws IllegalArgumentException {
		if (value < 0 || value > 255)
			throw new IllegalArgumentException(
					"Error writing unsigned byte: byte value may only range from 0 to 255.");

		// 0 -> 0
		// 127 -> 127
		// 128 -> -128
		// 255 -> -1

		if (value > 127)
			storageList.add(new Byte((byte) (value - 256)));
		else
			storageList.add(new Byte((byte) (value)));
	}

	public short readUnsignedByte() throws IllegalStateException {
		if (!validPos())
			throw new IllegalStateException(
					"Error reading unsigned byte, invalid list position specified for reading: "
							+ position);

		// -128 -> 128
		// -1 -> 255
		// 0 -> 0
		// 127 -> 127

		position++;
		return (short) ((listIt.next() + 256) % 256);

	}

	/**
	 * Write a signed short value to the list
	 * 
	 * @param value
	 *            the short value to be written
	 */
	public void writeShort(int value) throws IllegalArgumentException {
		ByteArrayOutputStream byteOut = new ByteArrayOutputStream(2);
		DataOutputStream dataOut = new DataOutputStream(byteOut);
		byte bytes[] = new byte[2];

		if (value < -32768 || value > 32768)
			throw new IllegalArgumentException(
					"Error writing short: short value may only range from -32768 to 32768.");

		try {
			dataOut.writeShort(value);
			dataOut.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
		bytes = byteOut.toByteArray();
		for (int i = 0; i < 2; i++)
			writeByte(bytes[i]);
	}

	/**
	 * Read a short value from the list
	 * 
	 * @return the read short value as an Integer
	 */
	public int readShort() throws IllegalStateException {
		ByteArrayInputStream byteIn;
		DataInputStream dataIn;
		byte content[] = new byte[2];
		int result = 0;

		for (int i = 0; i < 2; i++) {
			content[i] = (byte) readByte();
		}
		byteIn = new ByteArrayInputStream(content);
		dataIn = new DataInputStream(byteIn);
		try {
			result = dataIn.readShort();
			dataIn.close();
		} catch (IOException e) {
			e.printStackTrace();
		}

		return result;
	}

	/**
	 * Write an signed Integer to the list the int value is being split up into
	 * 4 bytes in msb first order
	 * 
	 * @param value
	 *            the int value to be written
	 */
	public void writeInt(int value) throws IllegalArgumentException {
		ByteArrayOutputStream byteOut = new ByteArrayOutputStream(4);
		DataOutputStream dataOut = new DataOutputStream(byteOut);
		byte bytes[] = new byte[4];

		try {
			dataOut.writeInt(value);
			dataOut.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
		bytes = byteOut.toByteArray();
		for (int i = 0; i < 4; i++)
			writeByte(bytes[i]);

	}

	/**
	 * Read a signed integer value, that was split up into 4 bytes
	 * 
	 * @return the read int value
	 */
	public int readInt() throws IllegalStateException {
		ByteArrayInputStream byteIn;
		DataInputStream dataIn;
		byte content[] = new byte[4];
		int result = 0;

		for (int i = 0; i < 4; i++) {
			content[i] = (byte) readByte();
		}
		byteIn = new ByteArrayInputStream(content);
		dataIn = new DataInputStream(byteIn);
		try {
			result = dataIn.readInt();
			dataIn.close();
		} catch (IOException e) {
			e.printStackTrace();
		}

		return result;
	}

	/**
	 * Read a signed integer value, that was split up into 8 bytes
	 * 
	 * @return the read int value
	 */
	public long readLong() throws IllegalStateException {
		ByteArrayInputStream byteIn;
		DataInputStream dataIn;
		byte content[] = new byte[8];
		long result = 0;

		for (int i = 0; i < 8; i++) {
			content[i] = (byte) readByte();
		}
		byteIn = new ByteArrayInputStream(content);
		dataIn = new DataInputStream(byteIn);
		try {
			result = dataIn.readLong();
			dataIn.close();
		} catch (IOException e) {
			e.printStackTrace();
		}

		return result;
	}

	/**
	 * Write a Float to the list the float value is beeing split up into 4 bytes
	 * in msb first order
	 * 
	 * @param value
	 *            the float value to be written
	 */
	public void writeFloat(float value) throws IllegalArgumentException {
		ByteArrayOutputStream byteOut = new ByteArrayOutputStream(4);
		DataOutputStream dataOut = new DataOutputStream(byteOut);
		byte bytes[] = new byte[4];

		try {
			dataOut.writeFloat(value);
			dataOut.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
		bytes = byteOut.toByteArray();
		for (int i = 0; i < 4; i++)
			writeByte(bytes[i]);

	}

	/**
	 * Read a Float value, that was split up into 4 bytes
	 * 
	 * @return the read float value
	 */
	public float readFloat() throws IllegalStateException {
		ByteArrayInputStream byteIn;
		DataInputStream dataIn;
		byte content[] = new byte[4];
		float result = 0;

		for (int i = 0; i < 4; i++) {
			content[i] = (byte) readByte();
		}
		byteIn = new ByteArrayInputStream(content);
		dataIn = new DataInputStream(byteIn);
		try {
			result = dataIn.readFloat();
			dataIn.close();
		} catch (IOException e) {
			e.printStackTrace();
		}

		return result;
	}

	/**
	 * Write a Double to the list the value is beeing split up into 8 bytes in
	 * msb first order
	 * 
	 * @param value
	 *            the double value to be written
	 */
	public void writeDouble(double value) throws IllegalArgumentException {
		ByteArrayOutputStream byteOut = new ByteArrayOutputStream(8);
		DataOutputStream dataOut = new DataOutputStream(byteOut);
		byte bytes[] = new byte[8];

		try {
			dataOut.writeDouble(value);
			dataOut.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
		bytes = byteOut.toByteArray();
		for (int i = 0; i < 8; i++)
			writeByte(bytes[i]);

	}

	/**
	 * Write a Long to the list the value is beeing split up into 8 bytes in msb
	 * first order
	 * 
	 * @param value
	 *            the double value to be written
	 */
	public void writeLong(long value) throws IllegalArgumentException {
		ByteArrayOutputStream byteOut = new ByteArrayOutputStream(8);
		DataOutputStream dataOut = new DataOutputStream(byteOut);
		byte bytes[] = new byte[8];

		try {
			dataOut.writeLong(value);
			dataOut.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
		bytes = byteOut.toByteArray();
		for (int i = 0; i < 8; i++)
			writeByte(bytes[i]);

	}

	/**
	 * Read a Double value, that was split up into 8 bytes
	 * 
	 * @return the read double value
	 */
	public double readDouble() throws IllegalStateException {
		ByteArrayInputStream byteIn;
		DataInputStream dataIn;
		byte content[] = new byte[8];
		double result = 0;

		for (int i = 0; i < 8; i++) {
			content[i] = (byte) readByte();
		}
		byteIn = new ByteArrayInputStream(content);
		dataIn = new DataInputStream(byteIn);
		try {
			result = dataIn.readDouble();
			dataIn.close();
		} catch (IOException e) {
			e.printStackTrace();
		}

		return result;
	}

	/**
	 * Write a string to the list by encoding the characters in UTF-8
	 * 
	 * @param value
	 *            the String to be written
	 */
	public void writeStringUTF8(String value) throws IllegalArgumentException {
		writeString(value, "UTF-8");
	}

	/**
	 * Write a string to the list by encoding the characters in ASCII
	 * 
	 * @param value
	 *            the String to be written
	 */
	public void writeStringASCII(String value) throws IllegalArgumentException {
		writeString(value, "US-ASCII");
	}

	/**
	 * Write a string to the list by encoding the characters in ISO-LATIN1
	 * 
	 * @param value
	 *            the String to be written
	 */
	public void writeStringISOLATIN1(String value)
			throws IllegalArgumentException {
		writeString(value, "ISO-8859-1");
	}

	/**
	 * Write a string to the list by encoding the characters in UTF-16 Big
	 * Endian
	 * 
	 * @param value
	 *            the String to be written
	 */
	public void writeStringUTF16BE(String value)
			throws IllegalArgumentException {
		writeString(value, "UTF-16BE");
	}

	/**
	 * Write a string to the list by encoding the characters in UTF-16 Little
	 * Endian
	 * 
	 * @param value
	 *            the String to be written
	 */
	public void writeStringUTF16LE(String value)
			throws IllegalArgumentException {
		writeString(value, "UTF-16LE");
	}

	private void writeString(String value, String charset)
			throws IllegalArgumentException {
		byte bytes[];

		try {
			bytes = value.getBytes(charset);
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
			return;
		}

		writeInt(value.length());

		for (int i = 0; i < bytes.length; i++)
			writeByte(bytes[i]);
	}

	/**
	 * Read a String from the list, that was encoded using UTF-8
	 * 
	 * @return the read String
	 */
	public String readStringUTF8() throws IllegalArgumentException {
		return readString("UTF-8");
	}

	/**
	 * Read a String from the list, that was encoded using ASCII
	 * 
	 * @return the read String
	 */
	public String readStringASCII() throws IllegalArgumentException {
		return readString("US-ASCII");
	}

	/**
	 * Read a String from the list, that was encoded using ISO-LATIN1
	 * 
	 * @return the read String
	 */
	public String readStringISOLATIN1() throws IllegalArgumentException {
		return readString("ISO-8859-1");
	}

	/**
	 * Read a String from the list, that was encoded using UTF-16 Big Endian
	 * 
	 * @return the read String
	 */
	public String readStringUTF16BE() throws IllegalArgumentException {
		return readString("UTF-16BE");
	}

	/**
	 * Read a String from the list, that was encoded using UTF-16 Little Endian
	 * 
	 * @return the read String
	 */
	public String readStringUTF16LE() throws IllegalArgumentException {
		return readString("UTF-16LE");
	}

	private String readString(String charset) throws IllegalStateException {
		byte content[];
		String result = new String("");
		int length;

		length = readInt();
		content = new byte[length];
		for (int i = 0; i < length; i++) {
			content[i] = (byte) readByte();
		}

		try {
			result = new String(content, charset);

		} catch (UnsupportedEncodingException e) {
			assert false : e;
		}

		return result;
	}

	public void reset() {
		storageList.clear();
		init();
	}

	/**
	 * Retrieve the Size of the internal storage list
	 * 
	 * @return size of the storage list
	 */
	public int size() {
		return storageList.size();
	}

	/**
	 * Retrieve the internal list that is used to store the data
	 * 
	 * @return the internal storage list
	 */
	public List<Byte> getStorageList() {
		return Collections.unmodifiableList(storageList);
	}

	private void init() {
		position = 0;
		listIt = storageList.listIterator();
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < storageList.size(); i++) {
			if (i == position)
				sb.append("[");
			else
				sb.append(" ");

			sb.append(String.format("%02X", storageList.get(i)));
		}

		return sb.toString();
	}
}
